﻿// <copyright file="AbstractMod.cs" company="SingularityShift">
// Copyright (c) 2009 SingularityShift.com
// </copyright>
// <author>Ben McClure</author>
// <email>ben@singularityshift.com</email>
// <date>2009-08-14</date>
// <summary>Implements most of the IMod functionality for other classes to use</summary>
namespace GameLib.Games
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.IO;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Xml.Serialization;
    using GameLib.Games.Components;
    using GameLib.Games.Util;

    /// <summary>   
    /// <para>
    /// This abstract class provides most of the functionality for an implmenting mod type.
    /// </para>
    /// <para>
    /// Mods need to provide a few base things, such as methods to instantiate the mod class with ideal conditions.
    /// </para> 
    /// </summary>
    /// <remarks>   Ben McClure, 11/29/2010. </remarks>
    [Serializable()]
    [XmlInclude(typeof(ModConflict)), XmlInclude(typeof(ModDependency)), XmlInclude(typeof(ModMessage))]
    public abstract class AbstractMod : IMod
    {
        /// <summary>
        /// The internal list of conflicts for this mod
        /// </summary>
        private readonly IList<IModConflict> _conflicts = new BindingList<IModConflict>();

        /// <summary>
        /// The internal list of dependencies for this mod
        /// </summary>
        private IList<IModDependency> dependencies = new BindingList<IModDependency>();

        /// <summary>
        /// The internal list of messages for this mod
        /// </summary>
        private IList<IModMessage> messages = new BindingList<IModMessage>();

        /// <summary>
        /// The internal string for the Id
        /// </summary>
        private string id;

        /// <summary>
        /// The internal string for the Title
        /// </summary>
        private string title;

        /// <summary>
        /// The type of the mod ('mod', 'patch', 'uninstaller', etc)
        /// </summary>
        private string type;

        /// <summary>
        /// The internal string for the File
        /// </summary>
        private string file;

        /// <summary>
        /// The internal string for the Notes field
        /// </summary>
        private string notes;

        /// <summary>
        /// The internal string for the Description field
        /// </summary>
        private string description;

        /// <summary>
        /// The internal Uri
        /// </summary>
        private System.Uri uri;

        /// <summary>
        /// Thrown when anything about a mod is changed
        /// </summary>
        public event EventHandler<ModChangedEventArgs> ModChanged;

        /// <summary>
        /// Gets or sets the IGame that owns (is the parent of) this mod.
        /// </summary>
        [XmlIgnore]
        public IGame Owner
        {
            get;
            set;
        }

        /// <summary>
        /// Gets a collection of potential or known conflicts with this mod.
        /// </summary>
        [XmlIgnore]
        public IList<IModConflict> Conflicts
        {
            get
            {
                return this._conflicts;
            }
        }

        /// <summary>
        /// Gets a collection of dependencies for this mod.
        /// </summary>
        [XmlIgnore]
        public IList<IModDependency> Dependencies
        {
            get
            {
                return this.dependencies;
            }
        }

        /// <summary>
        /// Gets a collection of messages regarding this mod.
        /// </summary>
        [XmlIgnore]
        public IList<IModMessage> Messages
        {
            get
            {
                return this.messages;
            }
        }

        /// <summary>
        /// <para>Gets or sets a unique identifier describing the mod. Often, this is just the mod's filename.</para>
        /// <para>The format of the Id is usually specified by the game the mod is for.</para>
        /// </summary>
        public string Id
        {
            get
            {
                return this.id;
            }

            set
            {
                string oldValue = this.id;
                this.id = value;
                this.OnModChanged(new ModChangedEventArgs(ModChangeType.IdChanged, oldValue));
            }
        }

        /// <summary>
        /// Gets or sets a human-readable title for the mod. When not provided, usually GameLib generates one based on the FileName.
        /// </summary>
        public string Name
        {
            get
            {
                return this.title;
            }

            set
            {
                string oldValue = this.title;
                this.title = value;
                this.OnModChanged(new ModChangedEventArgs(ModChangeType.TitleChanged, oldValue));
            }
        }

        /// <summary>
        /// Generally a short text describing what the mod does
        /// </summary>
        public string Description
        {
            get
            {
                return this.description;
            }

            set
            {
                string oldValue = this.description;
                this.description = value;
                this.OnModChanged(new ModChangedEventArgs(ModChangeType.DescriptionChanged, oldValue));
            }
        }

        /// <summary>
        /// Any notes for the user that don't warrant the important of a Message
        /// </summary>
        public string Notes
        {
            get
            {
                return this.notes;
            }

            set
            {
                string oldValue = this.notes;
                this.notes = value;
                this.OnModChanged(new ModChangedEventArgs(ModChangeType.NotesChanged, oldValue));
            }
        }

        /// <summary>
        /// Gets or sets the type of this mod as a type string defined by the application
        /// </summary>
        public string Type
        {
            get
            {
                return this.type;
            }

            set
            {
                string oldValue = this.type;
                this.type = value;
                this.OnModChanged(new ModChangedEventArgs(ModChangeType.TypeChanged, oldValue));
            }
        }

        /// <summary>
        /// Gets or sets the full path (or relative path to the game's ModDir) to this mod in the filesystem.
        /// </summary>
        [XmlIgnore]
        public string File
        {
            get
            {
                return this.file;
            }

            set
            {
                if (!String.IsNullOrEmpty(value))
                {
                    string oldValue = this.file;
                    this.file = value;
                    this.OnModChanged(new ModChangedEventArgs(ModChangeType.FileChanged, oldValue));
                }
            }
        }

        /// <summary>
        /// Gets the file name (without path) of the mod. This is usually equal to the id (unless the game uses another id scheme).
        /// </summary>
        [XmlIgnore]
        public virtual string FileName
        {
            get
            {
                return Path.GetFileName(this.File);
            }
        }

        /// <summary>
        /// Gets or sets a URI pointing to more information about the mod (usually a homepage URL)
        /// </summary>
        [XmlIgnore]
        public System.Uri Uri
        {
            get
            {
                return this.uri;
            }

            set
            {
                System.Uri oldValue = this.uri;
                this.uri = value;
                this.OnModChanged(new ModChangedEventArgs(ModChangeType.UriChanged, oldValue));
            }
        }

        /// <summary>
        /// Gets or sets a string representation of the Uri associated with this mod for serialization purposes
        /// </summary>
        [XmlAttribute("uri")]
        [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
        public string UriString
        {
            get { return Uri == null ? null : Uri.ToString(); }
            set { Uri = value == null ? null : new Uri(value); }
        }

        /// <summary>
        /// Gets a ModState enumeration defining whether (in this order) it has any conflicts, dependencies, or messages (or nothing).
        /// </summary>
        [XmlIgnore]
        public abstract ModStates State
        {
            get;
        }

        /// <summary>
        /// Determines the state of the mod based on its conflicts, dependencies, messages and status.
        /// </summary>
        /// <typeparam name="T">The type of IMod object this should be assumed to be</typeparam>
        /// <returns>A ModState value indicating the current state of the mod</returns>
        public virtual ModStates GetState<T>() where T : IMod
        {
            if (!IsActive)
            {
                return ModStates.None;
            }
            if (this.HasMatchingConflicts<T>(this.Owner.GetActiveMods<T>()))
            {
                return ModStates.Conflicts;
            }
            else if (this.HasMissingDependencies<T>(this.Owner.GetActiveMods<T>()))
            {
                return ModStates.Dependencies;
            }
            else if (this.HasMatchingMessages<T>(this.Owner.GetActiveMods<T>()))
            {
                return ModStates.Messages;
            }

            return ModStates.Active;
        }

        /// <summary>
        /// <para>Gets or sets a value indicating whether this mod is considered obsolete.</para>
        /// <para>Null values can be considered NOT obsolete, to avoid the overhead of always having to set this value when it's usually unused.</para>
        /// </summary>
        public bool Obsolete
        {
            get;
            set;
        }

        /// <summary>
        /// <para>Gets or sets a value indicating whether this mod is considered outdate.</para>
        /// <para>This should be set it:
        /// - This is a known older version of the mod than the current version (or than any other version in the current ModList usually)
        /// - There is something else that should replace this (add a Message about it)
        /// - This is no longer working in the latest stable version of the game</para>
        /// </summary>
        public bool Outdated
        {
            get;
            set;
        }

        /// <summary>
        /// <para>Gets or sets a value indicating whether or not this mod is a patch for another mod.</para>
        /// <para>Set this value to true if:
        /// - This mod is designed to modify another mod (but the other mod is still required)
        /// - This mod is designed for compatibility between two or more other mods
        /// - This mod is a patch for the Game it is designed for (eg. it fixes things in it or 'patches' it in some way that changes the original content)</para>
        /// </summary>
        public bool Patch
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a value indicating whether this mod is recognized by the application in a way defined by the application
        /// </summary>
        public bool Recognized
        {
            get;
            set;
        }

        /// <summary>
        /// <para>Gets or sets a value indicating whether this mod is invariably required to be active in this mod list.</para>
        /// <para>Set this value to true if:
        /// - Other mods depend on this mod which are also required (or cannot be deactivated for any reason)
        /// - This mod is required for the game to operate
        /// - This mod is required for the implementing class's uses</para>
        /// </summary>
        public bool Required
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a value indicating whether this mod is currently active in its owner IGame.
        /// </summary>
        [XmlIgnore]
        public virtual bool IsActive
        {
            get
            {
                return this.Owner.IsActive(this);
            }

            set
            {
                if (value)
                {
                    this.Owner.EnableMod(this);
                }
                else
                {
                    this.Owner.DisableMod(this);
                }
            }
        }

        /// <summary>
        /// Adds an IModConflict to the game's list of known conflicts.
        /// </summary>
        /// <param name="conflict">The IModConflict object to add to the list of known conflicts.</param>
        public void AddConflict(IModConflict conflict)
        {
            lock (this._conflicts)
            {
                conflict.Owner = this;
                this._conflicts.Remove(conflict);
                this._conflicts.Add(conflict);
            }

            this.OnModChanged(new ModChangedEventArgs(ModChangeType.ConflictAdded, conflict));
        }

        /// <summary>
        /// Adds an IModDependency to the game's list of known dependencies.
        /// </summary>
        /// <param name="dependency">The IModDependency object to add to the list of known dependencies.</param>
        public void AddDependency(IModDependency dependency)
        {
            lock (this.dependencies)
            {
                dependency.Owner = this;
                this.dependencies.Remove(dependency);
                this.dependencies.Add(dependency);
            }

            this.OnModChanged(new ModChangedEventArgs(ModChangeType.DependencyAdded, dependency));
        }

        /// <summary>
        /// Adds an IModMessage to the game's list of known dependencies.
        /// </summary>
        /// <param name="message">The IModMessage object to add to the list of messages about this mod.</param>
        public void AddMessage(IModMessage message)
        {
            lock (this.messages)
            {
                message.Owner = this;
                this.messages.Remove(message);
                this.messages.Add(message);
            }

            this.OnModChanged(new ModChangedEventArgs(ModChangeType.MessageAdded, message));
        }

        /// <summary>
        /// Returns a generic list of IMod objects from the provided IModList which match any of this mod's Conflicts.
        /// </summary>
        /// <param name="modList">The collection of mods to check for conflicts against</param>
        /// <returns>A collection of IMod objects which match this mod's Conflicts</returns>
        public IModCollection<T> FindMatchingConflicts<T>(IModCollection<T> modList) where T : IMod
        {
            IModCollection<T> result = new ModCollection<T>((IGame<T>)this.Owner);
            foreach (IModConflict conflict in this.Conflicts)
            {
                if (conflict.Matches(modList))
                {
                    foreach (T mod in conflict.Match(modList))
                    {
                        if (!mod.Equals(this))
                        {
                            result.Add(mod);
                        }
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Returns a generic list of IModConflict items from this mod that match one or more mods in the provided IModList.
        /// </summary>
        /// <param name="modList">The IModList object to check for conflicts against.</param>
        /// <returns>A collection of IModConflict objects which match the provided IModList</returns>
        public IList<IModConflict> FindConflictsWithMatches<T>(IModCollection<T> modList) where T : IMod
        {
            IList<IModConflict> result = new Collection<IModConflict>();
            foreach (IModConflict conflict in this.Conflicts)
            {
                if (conflict.Matches(modList))
                {
                    result.Add(conflict);
                }
            }

            return result;
        }

        public IList<IModConflict> FindConflictsWithMatches<T>(T mod) where T : IMod
        {
            IList<IModConflict> result = new Collection<IModConflict>();
            foreach (IModConflict conflict in this.Conflicts)
            {
                if (conflict.Matches(mod))
                {
                    result.Add(conflict);
                }
            }

            return result;
        }

        /// <summary>
        /// Returns a generic list of IModDependency items from this mod that are missing from the provided IModList.
        /// </summary>
        /// <param name="modList">The IModList object to check for dependencies against.</param>
        /// <returns>A collection of IModDependency objects which don't match the provided IModList</returns>
        public IList<IModDependency> FindDependenciesWithoutMatches<T>(IModCollection<T> modList) where T : IMod
        {
            IList<IModDependency> result = new Collection<IModDependency>();
            foreach (IModDependency dependency in this.Dependencies)
            {
                if (!dependency.Matches(modList))
                {
                    result.Add(dependency);
                }
            }

            return result;
        }

        /// <summary>
        /// Returns a generic list of IModMessage items from this mod that match the provided IModList.
        /// </summary>
        /// <param name="modList">The IModList object to check for matching messages against.</param>
        /// <returns>A collection of IModMessage objects which match the provided IModList</returns>
        public IList<IModMessage> FindMessagesWithMatches<T>(IModCollection<T> modList) where T : IMod
        {
            IList<IModMessage> result = new Collection<IModMessage>();
            foreach (IModMessage message in this.Messages)
            {
                if (message.Matches(modList))
                {
                    result.Add(message);
                }
            }

            return result;
        }

        /// <summary>
        /// Returns a list of IMods from the provided IModList which match any of this mod's Messages.
        /// </summary>
        /// <param name="modList">The IModList to check for messages against.</param>
        /// <returns>A collection of IMod objects from the provided IModList which match this mod's Messages</returns>
        public IModCollection<T> FindMatchingMessages<T>(IModCollection<T> modList) where T : IMod
        {
            IModCollection<T> result = new ModCollection<T>((IGame<T>)this.Owner);
            foreach (IModConflict message in this.messages)
            {
                if (message.Matches(modList, this))
                {
                    foreach (T mod in message.Match(modList))
                    {
                        if (!mod.Equals(this))
                        {
                            result.Add(mod);
                        }
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Returns a list of IMods from the provided IModList which match any of this mod's Dependencies.
        /// </summary>
        /// <param name="modList">The IModList to check for dependencies against.</param>
        /// <returns>A collection of IMods from the provided IModList which match any of this mod's Dependencies</returns>
        public IModCollection<T> FindMatchingDependencies<T>(IModCollection<T> modList) where T : IMod
        {
            IModCollection<T> result = new ModCollection<T>((IGame<T>)this.Owner);
            foreach (IModConflict dependency in this.dependencies)
            {
                if (dependency.Matches(modList, this))
                {
                    foreach (T mod in dependency.Match(modList))
                    {
                        if (!mod.Equals(this))
                        {
                            result.Add(mod);
                        }
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Returns a list of IModDependencies from this mod's Dependencies which are missing from the provided IModList.
        /// </summary>
        /// <param name="modList">The IModList to check for dependencies against.</param>
        /// <returns>A collection of IModDependencies from this mod which are missing from the provided IModList</returns>
        public IList<IModDependency> FindMissingDependencies<T>(IModCollection<T> modList) where T : IMod
        {
            IList<IModDependency> result = new Collection<IModDependency>();
            foreach (IModDependency dependency in this.dependencies)
            {
                if (!dependency.Matches(modList))
                {
                    result.Add(dependency);
                }
            }

            return result;
        }

        /// <summary>
        /// <para>Determines whether this mod has any conflicts matching the provided IModList.</para>
        /// <para>Upon detection of the first matching conflict, this function returns true.</para>
        /// </summary>
        /// <param name="modList">The IModList to check for conflicts against.</param>
        /// <returns>A bool indicating whether or not this mod has any matching conflicts.</returns>
        public bool HasMatchingConflicts<T>(IModCollection<T> modList) where T : IMod
        {
            if (this._conflicts.Count <= 0)
            {
                return false;
            }

            foreach (IModConflict conflict in this._conflicts)
            {
                foreach (T mod in modList)
                {
                    if (!mod.Equals((IMod)this))
                    {
                        if (conflict.Matches(mod))
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// <para>Determines whether this mod has any messages matching the provided IModList.</para>
        /// <para>Upon detection of the first matching message, this function returns true.</para>
        /// </summary>
        /// <param name="modList">The IModList to check for messages against.</param>
        /// <returns>A bool indicating whether or not this mod has any matching messages.</returns>
        public bool HasMatchingMessages<T>(IModCollection<T> modList) where T : IMod
        {
            if (this.messages.Count <= 0)
            {
                return false;
            }

            foreach (IModMessage message in this.messages)
            {
                if (string.IsNullOrEmpty(message.FilePattern))
                {
                    return true;
                }

                foreach (T mod in modList)
                {
                    if (!mod.Equals((IMod)this))
                    {
                        if (message.Matches(mod))
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// <para>Determines whether this mod has any dependencies matching the provided IModList.</para>
        /// <para>Upon detection of the first matching dependency, this function returns true.</para>
        /// </summary>
        /// <param name="modList">The IModList to check for dependencies against.</param>
        /// <returns>A bool indicating whether or not this mod has any matching dependencies.</returns>
        public bool HasMatchingDependencies<T>(IModCollection<T> modList) where T : IMod
        {
            if (this.dependencies.Count <= 0)
            {
                return false;
            }

            foreach (IModDependency dependency in this.dependencies)
            {
                foreach (T mod in modList)
                {
                    if (!mod.Equals((IMod)this))
                    {
                        if (dependency.Matches(mod))
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Overrides default Equals method
        /// </summary>
        /// <param name="obj">The object to compare against</param>
        /// <returns>A value indicating whether the provided object is equal to this object</returns>
        public override bool Equals(object obj)
        {
            if (!(obj is IMod))
            {
                return false;
            }

            return this.Equals((IMod)obj);
        }

        /// <summary>
        /// Determines equality of IMod objects
        /// </summary>
        /// <param name="mod">The IMod to compare this AbstractMod against</param>
        /// <returns>A value indicating whether the objects are equal</returns>
        public bool Equals(IMod mod)
        {
            return Id.Equals(mod.Id, StringComparison.CurrentCultureIgnoreCase);
        }

        /// <summary>
        /// Gets a unique hash code for this mod
        /// </summary>
        /// <returns>A hash code of the Id of the mod</returns>
        public override int GetHashCode()
        {
            return this.Id.GetHashCode();
        }

        /// <summary>
        /// <para>Determines whether this mod has any dependencies not found within the provided IModList.</para>
        /// <para>Upon detection of the first missing dependency, this function returns true.</para>
        /// </summary>
        /// <param name="modList">The IModList to check for missing dependencies against.</param>
        /// <returns>A bool indicating whether or not this mod has any missing dependencies.</returns>
        public bool HasMissingDependencies<T>(IModCollection<T> modList) where T : IMod
        {
            if (this.dependencies.Count <= 0)
            {
                return false;
            }

            bool matched;
            foreach (IModDependency dependency in this.dependencies)
            {
                matched = false;
                foreach (T mod in modList)
                {
                    if (!mod.Equals((IMod)this))
                    {
                        if (dependency.Matches(mod))
                        {
                            matched = true;
                        }
                    }
                }
                if (!matched)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Removes a conflict from this mod's Conflicts collection.
        /// </summary>
        /// <param name="index">The 0-based index of the conflict to remove.</param>
        public void RemoveConflict(int index)
        {
            if (this._conflicts.Count > index)
            {
                IModConflict conflict = this._conflicts[index];
                lock (this._conflicts)
                {
                    this._conflicts.RemoveAt(index);
                }

                this.OnModChanged(new ModChangedEventArgs(ModChangeType.ConflictRemoved, conflict));
            }
        }

        /// <summary>
        /// Removes a conflict from this mod's Conflicts collection.
        /// </summary>
        /// <param name="conflict">The IModConflict object to remove.</param>
        public void RemoveConflict(IModConflict conflict)
        {
            if (this._conflicts.Contains(conflict))
            {
                lock (this._conflicts)
                {
                    this._conflicts.Remove(conflict);
                }

                this.OnModChanged(new ModChangedEventArgs(ModChangeType.ConflictRemoved, conflict));
            }
        }

        /// <summary>
        /// Removes a dependency from this mod's Dependencies collection.
        /// </summary>
        /// <param name="index">The 0-based index of the dependency to remove.</param>
        public void RemoveDependency(int index)
        {
            if (this.dependencies.Count > index)
            {
                IModDependency dependency = this.dependencies[index];
                lock (this.dependencies)
                {
                    this.dependencies.RemoveAt(index);
                }

                this.OnModChanged(new ModChangedEventArgs(ModChangeType.DependencyRemoved, dependency));
            }
        }

        /// <summary>
        /// Removes a dependency from this mod's Dependencies collection.
        /// </summary>
        /// <param name="dependency">The IModDependency object to remove.</param>
        public void RemoveDependency(IModDependency dependency)
        {
            if (this.dependencies.Contains(dependency))
            {
                lock (this.dependencies)
                {
                    this.dependencies.Remove(dependency);
                }

                this.OnModChanged(new ModChangedEventArgs(ModChangeType.DependencyRemoved, dependency));
            }
        }

        /// <summary>
        /// Removes a message from this mod's Messages collection.
        /// </summary>
        /// <param name="index">The 0-based index of the message to remove.</param>
        public void RemoveMessage(int index)
        {
            if (this.messages.Count > index)
            {
                IModMessage message = this.messages[index];
                lock (this.messages)
                {
                    this.messages.RemoveAt(index);
                }

                this.OnModChanged(new ModChangedEventArgs(ModChangeType.MessageRemoved, message));
            }
        }

        /// <summary>
        /// Removes a message from this mod's Messages collection.
        /// </summary>
        /// <param name="message">The IModMessage object to remove.</param>
        public void RemoveMessage(IModMessage message)
        {
            if (this.messages.Contains(message))
            {
                lock (this.messages)
                {
                    this.messages.Remove(message);
                }

                this.OnModChanged(new ModChangedEventArgs(ModChangeType.MessageRemoved, message));
            }
        }

        /// <summary>
        /// <para>Creates a deep copy of this mod object (constructing a completely new, different-but-equal mod object).</para>
        /// <para>This REQUIRES the implementing IMod class to have a default, empty constructor.</para>
        /// </summary>
        /// <returns>A new IMod object that is of the same type and equal to the object it was called against, but is not the same.</returns>
        public object Clone()
        {
            BinaryFormatter bf = new BinaryFormatter();
            MemoryStream ms = new MemoryStream();
            bf.Serialize(ms, this);
            ms.Flush();
            ms.Position = 0;
            return (IMod)bf.Deserialize(ms);
        }

        /// <summary>
        /// A shortcut function which calls the ModChanged event
        /// </summary>
        /// <param name="eventArgs">The EventArgs to include with the event.</param>
        protected void OnModChanged(ModChangedEventArgs eventArgs)
        {
            if (this.ModChanged != null)
            {
                this.ModChanged(this, eventArgs);
            }
        }

        public void SetOwner(IGame game)
        {
            Owner = game;
        }
    }
}